from phonology.model import (ModelGroup, KeyValue, KeyValueExpression,
                             KeyValueExpressionOperators, Rule)


import xml.dom.minidom as minidom

__all__ = ['parse_xml_model']


# These next functions are used to ignore nodes that are simply empty text
def _this_or_next(node):
  if node is None:
    return None
  
  if node.nodeType == minidom.Node.COMMENT_NODE:
    return _next_child(node)
  
  if node.nodeType != minidom.Node.TEXT_NODE:
    return node
  
  data = node.data
  data = data.replace('\n', '')
  data = data.replace(' ', '')
  data = data.replace('\t', '')
  
  if data:
    return node
  else:
    return _next_child(node)


def _first_child(node):
  return _this_or_next(node.firstChild)


def _next_child(node):
  return _this_or_next(node.nextSibling)


def _all_children(node):
  node = _first_child(node)
  while node:
    yield node
    node = _next_child(node)


def _get_node_text(node):
  if node.nodeType == minidom.Node.TEXT_NODE:
    data = node.data
  elif node.nodeType == minidom.Node.ELEMENT_NODE:
    child = _first_child(node)
    if child:
      data = child.data
    else:
      return ""
  else:
    return ""
  
  start_index = 0
  end_index = len(data) - 1
  while start_index < len(data) and data[start_index] in ' \t\n':
    start_index += 1
  while end_index >= 0 and data[end_index] in ' \t\n':
    end_index -= 1
  
  return data[start_index : end_index + 1]


def _get_version(model):
  if not model.attributes.has_key('version'):
    raise Exception("You need to specify the version of the Phonology Model "
                    "language for the XML file")
  
  version = model.attributes['version'].value
  try:
    return int(version)
  except TypeError:
    raise Exception("The version must be an integer number.")


def _parse_pair_node(pair_node, unique=False):
  attrs = pair_node.attributes
  if not attrs.has_key("key"):
    raise Exception("A key is needed for every pair")
  key = attrs["key"].value
  
  children = _all_children(pair_node)
  
  # There are two (disjoint) possibilities:
  #  - There is a value attribute
  #  - There is one or more <value> elements
  
  value_in_attribute = None
  if attrs.has_key("value"):
    value_in_attribute = True
  
  values = []
  
  for child in children:
    tag_name = child.tagName
    if tag_name == "value":
      values.append(child)
    else:
      raise Exception("The element <%s> is not recognized inside a <pair>" % 
                      tag_name)
  
  if len(values) > 0 and value_in_attribute:
    raise Exception("You must not specify values for pairs as attributes "
                    "and child elements simultaneously.")
  elif len(values) > 0:
    value_in_attribute = False
  
  if value_in_attribute is None:
    raise Exception("Values for pairs must be specified either as an "
                    "attribute or child elements.")
  
  if value_in_attribute:
    values = [attrs["value"].value]
  else:
    values = [_get_node_text(i) for i in values]
  
  if unique and len(values) != 1:
    raise Exception("The pairs inside an expression must have a single value.")
  
  return [KeyValue(key, i) for i in values]


def _parse_expression_node(node):
  tag_name = node.tagName
  
  if tag_name == "pair":
    key_value = _parse_pair_node(node, True)
    return KeyValueExpression(KeyValueExpressionOperators.NONE, key_value)
  
  operator = getattr(KeyValueExpressionOperators, node.tagName.upper())
  pointers = [_parse_expression_node(i) for i in _all_children(node)]
  
  return KeyValueExpression(operator, pointers)


def _parse_premise(premise_node):
  expression_node = _first_child(premise_node)
  if expression_node:
    return _parse_expression_node(expression_node)
  else:
    return KeyValueExpression.default_expression


def _parse_object(object_node):
  # Can be a pair or a group
  
  node = _first_child(object_node)
  tag_name = node.tagName
  
  if tag_name == "group":
    attrs = node.attributes
    if not attrs.has_key("ref"):
      raise Exception("The group tag must have a ref attribute in the object "
                      "of a rule.")
    
    return ModelGroup(attrs["ref"].value, -1)
  
  return _parse_pair_node(node)


def _parse_version_1(model):
  groups = {}
  
  for node in _all_children(model):
    tag_name = node.tagName
    if tag_name == "group":
      if not node.attributes.has_key("name"):
        raise Exception("A group must have a name.")
      
      name = node.attributes["name"].value
      pairs = [i for pair_node in _all_children(node)
               for i in _parse_pair_node(pair_node)]
      group = ModelGroup(name, pairs) 
      groups[name] = group
      yield group
    
    elif tag_name == "rule":
      if not node.attributes.has_key("action"):
        raise Exception("A rule must have an action.")
      action = node.attributes["action"].value
      
      specs = list(_all_children(node))
      if len(specs) != 2:
        raise Exception("A rule must have exactly two specifications: a "
                        "premise and an object")
      
      one, two = specs
      premise, object = None, None
      if one.tagName == "premise": premise = one
      elif one.tagName == "object": object = one
      
      if two.tagName == "premise": premise = two
      elif two.tagName == "object": object = two
      
      if premise is None or object is None:
        raise Exception("A rule must have one premise and one object")
      
      premise = _parse_premise(premise)
      action = getattr(Rule, action.upper())
      object = _parse_object(object)
      
      if type(object) == ModelGroup:
        if object.name in groups:
          object = groups[object.name]
        else:
          raise Exception("Group %s does not exist." % object.name)
      
      yield Rule(premise, action, object)


def parse_xml_model(filename):
  """This function reads a file containing a XML version of a Phonology Model.
  It then proceeds by returning a generator that yields the several objects
  present in the file, which will then be used by the PhonologyModel object to
  recreate the model in a full machine-readable format.
  """
  
  doc = minidom.parse(filename)
  model = _first_child(doc)
  
  # Get the version of the XML model file
  version = _get_version(model)
  if version != 1:
    raise Exception("The only recognized version is 1")
  
  # Call the corresponding parser function
  function = globals()["_parse_version_%i" % version]
  return function(model)

